package com.jneagle.xlstool.bzwgjmx.view.gui;

import com.dwarfeng.dutil.basic.cna.model.ReferenceModel;
import com.dwarfeng.dutil.basic.cna.model.obs.ReferenceAdapter;
import com.dwarfeng.dutil.basic.gui.swing.MappingTableModel;
import com.dwarfeng.dutil.basic.gui.swing.SwingUtil;
import com.dwarfeng.subgrade.stack.exception.ServiceException;
import com.jneagle.xlstool.bzwgjmx.sdk.util.ServiceExceptionCodes;
import com.jneagle.xlstool.bzwgjmx.stack.bean.entity.ExportData;
import com.jneagle.xlstool.bzwgjmx.stack.bean.entity.ExportErrorInfo;
import com.jneagle.xlstool.bzwgjmx.stack.service.DataExportService;
import com.jneagle.xlstool.bzwgjmx.stack.service.ExportDataMaintainService;
import com.jneagle.xlstool.bzwgjmx.stack.service.ExportErrorInfoMaintainService;
import com.jneagle.xlstool.bzwgjmx.stack.structure.ProgressObserver;
import com.jneagle.xlstool.bzwgjmx.stack.structure.ProgressStatus;
import com.jneagle.xlstool.bzwgjmx.view.handler.I18nHandler;
import com.jneagle.xlstool.bzwgjmx.view.handler.MessageHandler;
import com.jneagle.xlstool.bzwgjmx.view.handler.ModalHandler;
import com.jneagle.xlstool.bzwgjmx.view.structure.ModalItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.DependsOn;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.io.File;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;

/**
 * 数据导出面板。
 *
 * @author DwArFeng
 * @since 1.0.0
 */
@Component
@DependsOn("viewConfiguration")
public class DataExportPanel extends JPanel {

    private static final long serialVersionUID = -3090595443070263433L;
    private static final Logger LOGGER = LoggerFactory.getLogger(DataExportPanel.class);

    private final ApplicationContext ctx;

    private final DataExportService dataExportService;
    private final ExportDataMaintainService exportDataMaintainService;
    private final ExportErrorInfoMaintainService exportErrorInfoMaintainService;

    private final I18nHandler i18nHandler;
    private final Locale uiLocale;
    private final ModalHandler modalHandler;
    private final ThreadPoolTaskExecutor executor;
    private final MessageHandler messageHandler;

    private final ReferenceModel<File> structuredFileModel;
    private final ReferenceModel<String> structuredPasswordModel;
    private final ReferenceModel<String> notificationModel;
    private final ReferenceModel<ProgressStatus> progressStatusModel;
    private final MappingTableModel<ExportData> exportDataTableModel;
    private final MappingTableModel<ExportErrorInfo> exportErrorInfoTableModel;
    private final ReferenceModel<File> exportFileModel;
    private final ReferenceModel<String> exportPasswordModel;

    private final ExportDataErrorDialog exportDataErrorDialog;

    private final JTextField exportFileTextField = new JTextField();
    private final JButton selectFileButton = new JButton();
    private final JPasswordField passwordField = new JPasswordField();
    private final JButton saveFileButton = new JButton();
    private final JTextField resultTextField = new JTextField();
    private final JButton showErrorButton = new JButton();
    private final JTable exportDataTable = new JTable();

    private final ExportFileObserver exportFileObserver = new ExportFileObserver();
    private final PasswordObserver passwordObserver = new PasswordObserver();
    private final ExportDataSaveObserver exportDataSaveObserver = new ExportDataSaveObserver();
    private final StructuredDataTableModelListener structuredDataTableModelListener
            = new StructuredDataTableModelListener();
    private final StructuredErrorInfoTableModelListener structuredErrorInfoTableModelListener
            = new StructuredErrorInfoTableModelListener();

    public DataExportPanel(
            ApplicationContext ctx,
            DataExportService dataExportService,
            ExportDataMaintainService exportDataMaintainService,
            ExportErrorInfoMaintainService exportErrorInfoMaintainService,
            I18nHandler i18nHandler,
            Locale uiLocale,
            ModalHandler modalHandler,
            ThreadPoolTaskExecutor executor,
            MessageHandler messageHandler,
            @Qualifier("structuredFileModel") ReferenceModel<File> structuredFileModel,
            @Qualifier("structuredPasswordModel") ReferenceModel<String> structuredPasswordModel,
            @Qualifier("notificationModel") ReferenceModel<String> notificationModel,
            @Qualifier("progressStatusModel") ReferenceModel<ProgressStatus> progressStatusModel,
            @Qualifier("exportDataTableModel") MappingTableModel<ExportData> exportDataTableModel,
            @Qualifier("exportErrorInfoTableModel") MappingTableModel<ExportErrorInfo> exportErrorInfoTableModel,
            @Qualifier("exportFileModel") ReferenceModel<File> exportFileModel,
            @Qualifier("exportPasswordModel") ReferenceModel<String> exportPasswordModel,
            ExportDataErrorDialog exportDataErrorDialog
    ) {
        this.ctx = ctx;
        this.dataExportService = dataExportService;
        this.exportDataMaintainService = exportDataMaintainService;
        this.exportErrorInfoMaintainService = exportErrorInfoMaintainService;
        this.i18nHandler = i18nHandler;
        this.uiLocale = uiLocale;
        this.modalHandler = modalHandler;
        this.executor = executor;
        this.messageHandler = messageHandler;
        this.structuredFileModel = structuredFileModel;
        this.structuredPasswordModel = structuredPasswordModel;
        this.notificationModel = notificationModel;
        this.progressStatusModel = progressStatusModel;
        this.exportDataTableModel = exportDataTableModel;
        this.exportErrorInfoTableModel = exportErrorInfoTableModel;
        this.exportFileModel = exportFileModel;
        this.exportPasswordModel = exportPasswordModel;
        this.exportDataErrorDialog = exportDataErrorDialog;
    }

    @SuppressWarnings("DuplicatedCode")
    @PostConstruct
    public void postConstruct() {
        // 初始化界面。
        GridBagLayout gridBagLayout = new GridBagLayout();
        gridBagLayout.columnWidths = new int[]{0, 0, 0};
        gridBagLayout.rowHeights = new int[]{0, 0, 0, 0};
        gridBagLayout.columnWeights = new double[]{0.0, 1.0, 0.0};
        gridBagLayout.rowWeights = new double[]{0.0, 0.0, 0.0, 1.0};
        setLayout(gridBagLayout);

        JLabel exportFileLabel = new JLabel(i18nHandler.getMessage("ui.label.069"));
        GridBagConstraints gbcLoadFileLabel = new GridBagConstraints();
        gbcLoadFileLabel.insets = new Insets(0, 0, 0, 5);
        gbcLoadFileLabel.anchor = GridBagConstraints.EAST;
        gbcLoadFileLabel.gridx = 0;
        gbcLoadFileLabel.gridy = 0;
        add(exportFileLabel, gbcLoadFileLabel);

        exportFileTextField.setEditable(false);
        GridBagConstraints gbcLoadFileTextField = new GridBagConstraints();
        gbcLoadFileTextField.insets = new Insets(0, 0, 0, 5);
        gbcLoadFileTextField.fill = GridBagConstraints.BOTH;
        gbcLoadFileTextField.gridx = 1;
        gbcLoadFileTextField.gridy = 0;
        add(exportFileTextField, gbcLoadFileTextField);

        selectFileButton.setAction(new SelectFileAction());
        selectFileButton.setText(i18nHandler.getMessage("ui.label.005"));
        GridBagConstraints gbcSelectFileButton = new GridBagConstraints();
        gbcSelectFileButton.insets = new Insets(0, 0, 0, 0);
        gbcSelectFileButton.fill = GridBagConstraints.BOTH;
        gbcSelectFileButton.gridx = 2;
        gbcSelectFileButton.gridy = 0;
        add(selectFileButton, gbcSelectFileButton);

        JLabel passwordLabel = new JLabel(i18nHandler.getMessage("ui.label.007"));
        GridBagConstraints gbcPasswordLabel = new GridBagConstraints();
        gbcPasswordLabel.insets = new Insets(0, 0, 0, 5);
        gbcPasswordLabel.anchor = GridBagConstraints.EAST;
        gbcPasswordLabel.gridx = 0;
        gbcPasswordLabel.gridy = 1;
        add(passwordLabel, gbcPasswordLabel);

        passwordField.getDocument().addDocumentListener(new PasswordDocumentListener());
        GridBagConstraints gbcPasswordField = new GridBagConstraints();
        gbcPasswordField.insets = new Insets(0, 0, 0, 5);
        gbcPasswordField.fill = GridBagConstraints.BOTH;
        gbcPasswordField.gridx = 1;
        gbcPasswordField.gridy = 1;
        add(passwordField, gbcPasswordField);

        saveFileButton.setAction(new SaveFileAction());
        saveFileButton.setText(i18nHandler.getMessage("ui.label.059"));
        GridBagConstraints gbcLoadFileButton = new GridBagConstraints();
        gbcLoadFileButton.insets = new Insets(0, 0, 0, 0);
        gbcLoadFileButton.fill = GridBagConstraints.BOTH;
        gbcLoadFileButton.gridx = 2;
        gbcLoadFileButton.gridy = 1;
        add(saveFileButton, gbcLoadFileButton);

        JLabel resultLabel = new JLabel(i18nHandler.getMessage("ui.label.018"));
        GridBagConstraints gbcResultLabel = new GridBagConstraints();
        gbcResultLabel.insets = new Insets(0, 0, 0, 5);
        gbcResultLabel.anchor = GridBagConstraints.EAST;
        gbcResultLabel.gridx = 0;
        gbcResultLabel.gridy = 2;
        add(resultLabel, gbcResultLabel);

        resultTextField.setEditable(false);
        GridBagConstraints gbcResultTextField = new GridBagConstraints();
        gbcResultTextField.insets = new Insets(0, 0, 0, 5);
        gbcResultTextField.fill = GridBagConstraints.BOTH;
        gbcResultTextField.gridx = 1;
        gbcResultTextField.gridy = 2;
        add(resultTextField, gbcResultTextField);

        showErrorButton.setAction(new ShowErrorDialogAction());
        showErrorButton.setText(i18nHandler.getMessage("ui.label.022"));
        GridBagConstraints gbcShowErrorButton = new GridBagConstraints();
        gbcShowErrorButton.insets = new Insets(0, 0, 0, 0);
        gbcShowErrorButton.fill = GridBagConstraints.BOTH;
        gbcShowErrorButton.gridx = 2;
        gbcShowErrorButton.gridy = 2;
        add(showErrorButton, gbcShowErrorButton);

        I18nTableCellRenderer structuredDataHeaderRender = ctx.getBean(
                I18nTableCellRenderer.class,
                ctx.getBean(I18nHandler.class),
                exportDataTable.getTableHeader().getDefaultRenderer()
        );
        exportDataTable.getTableHeader().setDefaultRenderer(structuredDataHeaderRender);
        exportDataTable.setModel(exportDataTableModel);
        JScrollPane structuredDataScrollPane = new JScrollPane();
        structuredDataScrollPane.setViewportView(exportDataTable);
        GridBagConstraints gbcStructuredDataTable = new GridBagConstraints();
        gbcStructuredDataTable.insets = new Insets(5, 0, 0, 0);
        gbcStructuredDataTable.fill = GridBagConstraints.BOTH;
        gbcStructuredDataTable.gridx = 0;
        gbcStructuredDataTable.gridy = 3;
        gbcStructuredDataTable.gridwidth = 3;
        add(structuredDataScrollPane, gbcStructuredDataTable);

        // 添加侦听器。
        exportFileModel.addObserver(exportFileObserver);
        exportPasswordModel.addObserver(passwordObserver);
        exportDataTableModel.addTableModelListener(structuredDataTableModelListener);
        exportErrorInfoTableModel.addTableModelListener(structuredErrorInfoTableModelListener);

        // 同步属性。
        syncProperties();
    }

    @SuppressWarnings("DuplicatedCode")
    private void syncProperties() {
        SwingUtil.invokeInEventQueue(() -> {
            File file = exportFileModel.get();
            exportFileTextField.setText(
                    Optional.ofNullable(file).map(File::getAbsolutePath).orElse("")
            );
            saveFileButton.setEnabled(Objects.nonNull(file));
            resultTextField.setText(i18nHandler.getMessage(
                    "ui.label.019",
                    new Object[]{exportDataTableModel.size(), exportErrorInfoTableModel.size()}
            ));
            showErrorButton.setEnabled(!exportErrorInfoTableModel.isEmpty());
        });
    }

    @PreDestroy
    public void preDestroy() {
        // 移除侦听器。
        exportFileModel.removeObserver(exportFileObserver);
        exportPasswordModel.removeObserver(passwordObserver);
        exportDataTableModel.removeTableModelListener(structuredDataTableModelListener);
        exportErrorInfoTableModel.removeTableModelListener(structuredErrorInfoTableModelListener);
    }

    public void loadData() {
        exportDataTableModel.clear();
        exportErrorInfoTableModel.clear();
        executor.submit(() -> {
            try {
                java.util.List<ExportData> exportDataList = exportDataMaintainService.lookupAsList();
                java.util.List<ExportErrorInfo> exportErrorInfoList = exportErrorInfoMaintainService.lookupAsList();
                exportDataTableModel.addAll(exportDataList);
                exportErrorInfoTableModel.addAll(exportErrorInfoList);
            } catch (Exception e) {
                messageHandler.error(this, "ui.label.031");
            }
        });
    }

    private class ExportFileObserver extends ReferenceAdapter<File> {

        @Override
        public void fireSet(File oldValue, File newValue) {
            SwingUtil.invokeInEventQueue(() -> {
                exportFileTextField.setText(
                        Optional.ofNullable(newValue).map(File::getAbsolutePath).orElse("")
                );
                saveFileButton.setEnabled(Objects.nonNull(newValue));
            });
        }
    }

    private class SelectFileAction extends AbstractAction {

        private static final long serialVersionUID = -1447459208994769258L;

        @SuppressWarnings("DuplicatedCode")
        @Override
        public void actionPerformed(ActionEvent e) {
            try {
                // 创建文件选择器并进行设置。
                JFileChooser jfc = new JFileChooser();
                jfc.setLocale(uiLocale);
                jfc.setFileSelectionMode(JFileChooser.FILES_ONLY);
                jfc.setFileFilter(new FileNameExtensionFilter(i18nHandler.getMessage("ui.label.006"), "xls"));
                jfc.setAcceptAllFileFilterUsed(false);
                // 如果已经有文件了，将当前路径设置为指定文件的路径。
                File currentFile = exportFileModel.get();
                if (Objects.nonNull(currentFile)) {
                    if (currentFile.isDirectory()) {
                        jfc.setCurrentDirectory(currentFile);
                    } else {
                        jfc.setCurrentDirectory(currentFile.getParentFile());
                    }
                }
                // 显示界面，让用户选择文件。
                int result = jfc.showOpenDialog(DataExportPanel.this);
                // 如果用户按下了确定键，则将文件模型设置为用户选择的文件。
                if (Objects.equals(result, JFileChooser.APPROVE_OPTION)) {
                    passwordField.setText("");
                    File file = jfc.getSelectedFile();
                    if (!file.getAbsolutePath().endsWith(".xls")) {
                        file = new File(file.getAbsolutePath() + ".xls");
                    }
                    exportFileModel.set(file);
                }
            } catch (Exception ex) {
                LOGGER.warn("选择文件时发生异常，异常信息如下: ", ex);
                messageHandler.error(DataExportPanel.this, "ui.label.009");
            }
        }
    }

    private class PasswordObserver extends ReferenceAdapter<String> {

        private boolean enabled = true;

        @Override
        public void fireSet(String oldValue, String newValue) {
            if (!enabled) {
                return;
            }
            SwingUtil.invokeInEventQueue(() -> passwordField.setText(newValue));
        }

        public boolean isEnabled() {
            return enabled;
        }

        public void setEnabled(boolean enabled) {
            this.enabled = enabled;
        }
    }

    private class PasswordDocumentListener implements DocumentListener {

        @Override
        public void insertUpdate(DocumentEvent e) {
            changePassword();
        }

        @Override
        public void removeUpdate(DocumentEvent e) {
            changePassword();
        }

        @Override
        public void changedUpdate(DocumentEvent e) {
            // Do nothing
        }

        private void changePassword() {
            passwordObserver.setEnabled(false);
            exportPasswordModel.set(new String(passwordField.getPassword()));
            passwordObserver.setEnabled(true);
        }
    }

    private class SaveFileAction extends AbstractAction implements Runnable {

        private static final long serialVersionUID = -6065224656800684487L;

        @Override
        public void actionPerformed(ActionEvent e) {
            // 在后台线程中执行方法，避免前端界面卡顿。
            executor.execute(this);
        }

        @Override
        public void run() {
            try {
                // 通过控制按钮的 enabled 属性，此处可以保证按钮按下时文件是有效的。
                File inputFile = structuredFileModel.get();
                File outputFile = exportFileModel.get();
                String inputPassword = structuredPasswordModel.get();
                String outputPassword = exportPasswordModel.get();

                // 保存文件读取的模态信息。
                modalHandler.setParsedValue(ModalItem.MODAL_STRUCTURED_DATA_LAST_LOAD_FILE, inputFile);
                modalHandler.setParsedValue(ModalItem.MODAL_STRUCTURED_DATA_LAST_LOAD_FLAG, true);

                // 更新通知面板标签文本。
                notificationModel.set(i18nHandler.getMessage("ui.label.061"));

                // 创建侦听
                dataExportService.addObserver(exportDataSaveObserver);
                // 调用接口读取文件，并捕获异常。
                try {
                    dataExportService.exportData(inputFile, outputFile, inputPassword, outputPassword);
                } catch (ServiceException ex) {
                    // 分析异常。
                    int code = ex.getCode().getCode();
                    if (Objects.equals(code, ServiceExceptionCodes.WRONG_PASSWORD.getCode())) {
                        messageHandler.warn(DataExportPanel.this, "ui.label.066");
                    } else {
                        messageHandler.warn(DataExportPanel.this, "ui.label.015");
                    }
                    // 更新通知面板标签文本。
                    notificationModel.set(i18nHandler.getMessage("ui.label.062"));
                    return;
                } finally {
                    // 移除侦听。
                    dataExportService.removeObserver(exportDataSaveObserver);
                }

                // 重新加载结构化数据。
                loadData();

                // 判断是否存在数据错误，如果有错误，则需要自动弹出错误对话框。
                if (exportErrorInfoTableModel.isEmpty()) {
                    notificationModel.set(i18nHandler.getMessage("ui.label.064"));
                } else {
                    notificationModel.set(i18nHandler.getMessage("ui.label.065"));
                }
            } catch (Exception ex) {
                LOGGER.warn("加载文件时发生异常，异常信息如下: ", ex);
                messageHandler.error(DataExportPanel.this, "ui.label.060");
            }
        }
    }

    private class ExportDataSaveObserver implements ProgressObserver {

        @Override
        public void onProgressChanged(ProgressStatus progressStatus) {
            progressStatusModel.set(progressStatus);
        }
    }

    private class StructuredDataTableModelListener implements TableModelListener {

        @Override
        public void tableChanged(TableModelEvent e) {
            SwingUtil.invokeInEventQueue(() -> resultTextField.setText(i18nHandler.getMessage(
                    "ui.label.019",
                    new Object[]{exportDataTableModel.size(), exportErrorInfoTableModel.size()}
            )));
        }
    }

    private class StructuredErrorInfoTableModelListener implements TableModelListener {

        @Override
        public void tableChanged(TableModelEvent e) {
            SwingUtil.invokeInEventQueue(() -> {
                resultTextField.setText(i18nHandler.getMessage(
                        "ui.label.019",
                        new Object[]{exportDataTableModel.size(), exportErrorInfoTableModel.size()}
                ));
                showErrorButton.setEnabled(!exportErrorInfoTableModel.isEmpty());
            });
        }
    }

    private class ShowErrorDialogAction extends AbstractAction {

        private static final long serialVersionUID = -1272840665555214513L;

        @Override
        public void actionPerformed(ActionEvent e) {
            exportDataErrorDialog.setLocationRelativeTo(ctx.getBean(MainFrame.class));
            exportDataErrorDialog.setVisible(true);
        }
    }
}
